package org.feit.findword.game;

import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;

import org.feit.findword.R;
import org.feit.findword.Synchronizer;
import org.feit.findword.trie.CompressedTrieEn;
import org.feit.findword.trie.CompressedTrieMk;
import org.feit.findword.trie.LettersMap;
import org.feit.findword.trie.Solution;
import org.feit.findword.trie.WordFilter;

import android.content.Context;
import android.content.SharedPreferences;
import android.media.AudioManager;
import android.media.SoundPool;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.Log;

public class Game implements Synchronizer.Counter {

	private static final String TAG = "Game";
	private int mTimeRemaining;
	private int mMaxTime;

	private int mMaxTimeRemaining;

	private Board mBoard;
	private LinkedHashMap<String, Solution> mSolutions;
	private RotateHandler mRotateHandler;
	private AudioManager mAudioManager;
	private SoundPool mSoundPool;
	private int mSoundHit;
	private int mSoundMiss;
	private int mSoundDuplicate;
	private int mSoundTap;
	private int mSoundEndLevel;
	private int mSoundClick;
	private int mSoundClack;
	private String mLanguage;
	private int mLevelTotalPoints;
	private int mCurrentLevel;
	private int mLevelPoints;
	private int mTimeRemained;
	private int mPreviousLevelPoints;
	private int mSeconds;

	public enum GameStatus {
		GAME_STARTING, GAME_RUNNING, GAME_PAUSED, GAME_NEW_LEVEL, GAME_FINISHED
	};

	private int setLevelPoints() {
		int totalPoints = 0;
		for (String solution : mSolutions.keySet()) {
			totalPoints += wordPoints(solution.length());
		}
		mPreviousLevelPoints = mLevelTotalPoints;
		mLevelTotalPoints += mCurrentLevel * 0.05 * totalPoints;
		return mLevelTotalPoints;
	}

	private boolean hasNextLevel() {
		int points = 0;
		for (String solution : mSolutions.keySet()) {
			points += wordPoints(solution.length());
		}
		int temp = (int) (mLevelTotalPoints + (mCurrentLevel * 0.05 * points));
		return mPoints < temp;
	}

	public int getLevelPercent() {
		return mCurrentLevel * 5;
	}

	public int getPoints() {
		return mPoints;
	}

	public int getTotalPoints() {
		return mLevelTotalPoints;
	}

	public int getCurrentPercent() {
		return (mPoints - mPreviousLevelPoints) * 100
				/ (mLevelTotalPoints - mPreviousLevelPoints);
	}

	public int getCurrentLevel() {
		return mCurrentLevel;
	}

	public int getLevelPoints() {
		return mLevelPoints;
	}

	public int getTimeRemained() {
		return mTimeRemained;
	}

	public static int wordPoints(int length) {
		return (int) Math.pow(2.0, (double) length) * 50;
	}

	private GameStatus mStatus;

	private LinkedList<String> mWordsList;
	private LinkedList<String> mGuessedWordsList;
	private LinkedHashSet<String> mWordsUsed;
	private int mWordCount;
	private int mPoints;

	private Date mStart;
	private Context mContext;

	private int mBoardSize; // using an int so I can use much larger boards
	// later
	private int mMinWordLength;

	public Game(Context context) {
		mContext = context;
		mStatus = GameStatus.GAME_STARTING;
		mWordCount = 0;
		mPoints = 0;
		mLevelTotalPoints = 0;
		mPreviousLevelPoints = 0;
		mLevelPoints = 0;
		mCurrentLevel = 1;
		loadPreferences(context);
		generateBoard();

		mTimeRemaining = getMaxTimeRemaining();
		mMaxTime = getMaxTimeRemaining();

		mWordsList = new LinkedList<String>();
		mWordsUsed = new LinkedHashSet<String>();
		mGuessedWordsList = new LinkedList<String>();

		setLevelPoints();
	}

	private void generateBoard() {
		LettersGenerator lettersGenerator = null;
		if (mLanguage.equals("MK")) {
			lettersGenerator = new LettersGenerator(mContext.getResources()
					.openRawResource(R.raw.letters_mk));
		} else {
			lettersGenerator = new LettersGenerator(mContext.getResources()
					.openRawResource(R.raw.letters_en));
		}

		int rc = (int) Math.sqrt(mBoardSize);

		setBoard(lettersGenerator.generateBoard(rc, rc, mLanguage.equals("MK")));
		if (!hasNextLevel()) {
			Log.d(TAG, "Re generating board: " + mLevelTotalPoints);
			generateBoard();
		}
	}

	public Game(Context c, SharedPreferences prefs) {
		mStatus = GameStatus.GAME_STARTING;
		mWordCount = 0;

		mContext = c;
		loadPreferences(c);
		String[] board = prefs.getString("gameBoard", null).split(",");
		mBoardSize = prefs.getInt("boardSize", 16);
		mLanguage = prefs.getString("dict", "MK");
		try {
			int rc = (int) Math.sqrt(mBoardSize);
			setBoard(new GenericBoard(rc, rc, board, mLanguage.equals("MK")));

			mTimeRemaining = prefs.getInt("timeRemaining", 0);
			Log.d(TAG, "Time remaining: " + mTimeRemaining);
			mMaxTime = mTimeRemaining;
			mMaxTimeRemaining = prefs.getInt("maxTimeRemaining", 18000);

			// Correct the time remaining.
			String[] wordArray = prefs.getString("words", null).split(",");
			String[] guessedWordsArray = prefs.getString("guessed_words", null)
					.split(",");
			mWordsList = new LinkedList<String>();
			mWordsUsed = new LinkedHashSet<String>();
			mGuessedWordsList = new LinkedList<String>();
			for (int i = 0; i < wordArray.length; i++) {
				mWordsList.add(wordArray[i]);
				mWordsUsed.add(wordArray[i]);
			}
			for (int i = 0; i < guessedWordsArray.length; i++) {
				mGuessedWordsList.add(guessedWordsArray[i]);
			}
			mWordCount = prefs.getInt("wordCount", 0);
			mPoints = prefs.getInt("score", 0);
			mCurrentLevel = prefs.getInt("level", 1);
			mLevelTotalPoints = prefs.getInt("level_total", 0);
			Log.d(TAG, "loading level total: " + mLevelTotalPoints);
			mStatus = GameStatus.GAME_STARTING;

		} catch (Exception e) {
			Log.e(TAG, "Error Restoring Saved Game", e);
			mStatus = GameStatus.GAME_FINISHED;
		}
	}

	public Game(Context c, Bundle bun) {
		this(c, bun, false);
	}

	public Game(Context c, Bundle bun, boolean adjustTime) {
		mStatus = GameStatus.GAME_STARTING;
		mWordCount = 0;

		mContext = c;
		loadPreferences(c);
		mBoardSize = bun.getInt("boardSize");
		Log.d(TAG, "bun.getString(\"gameBoard\")" + bun.getString("gameBoard"));
		mLanguage = bun.getString("dict");
		try {
			String[] board = bun.getString("gameBoard").split(",");
			int rc = (int) Math.sqrt(mBoardSize);
			setBoard(new GenericBoard(rc, rc, board, mLanguage.equals("MK")));

			mMaxTimeRemaining = bun.getInt("maxTimeRemaining", 18000);
			if (adjustTime) {
				// Log.d(TAG,"adjustTime");
				Date now = new Date();
				mTimeRemaining = Math.max(mMaxTimeRemaining
						- (int) (now.getTime() - bun.getLong("startTime", 0))
						/ 10, 0);
				mMaxTime = mTimeRemaining;
				mStart = new Date(bun.getLong("startTime", 0));
			} else {
				mTimeRemaining = bun.getInt("timeRemaining", 0);
				mMaxTime = mTimeRemaining;
				mStart = new Date();
			}

			// Correct the time remaining.
			String[] wordArray = bun.getString("words").split(",");
			String[] guessedWordsArray = bun.getString("guessed_words").split(
					",");
			mWordsList = new LinkedList<String>();
			mWordsUsed = new LinkedHashSet<String>();
			mGuessedWordsList = new LinkedList<String>();
			for (int i = 0; i < wordArray.length; i++) {
				mWordsList.add(wordArray[i]);
				mWordsUsed.add(wordArray[i]);
			}
			for (int i = 0; i < guessedWordsArray.length; i++) {
				mGuessedWordsList.add(guessedWordsArray[i]);
			}
			mWordCount = bun.getInt("wordCount", 0);
			mPoints = bun.getInt("score", 0);
			mCurrentLevel = bun.getInt("level", 1);
			mLevelTotalPoints = bun.getInt("level_total", 0);
			mStatus = GameStatus.valueOf(bun.getString("status"));
			Log.d(TAG, "status:" + mStatus);

		} catch (Exception e) {
			Log.e(TAG, "Error Restoring Saved Game", e);
			mStatus = GameStatus.GAME_FINISHED;
		}
	}

	public void setBoard(Board b) {
		mBoard = b;
		mBoardSize = b.getSize();
		setMinWordLength(mBoardSize);
		initializeDictionary();
	}

	void setMinWordLength(int boardSize) {
		mMinWordLength = Math.max((int) Math.sqrt(boardSize) - 3, 2);
		Log.d(TAG, "min word: " + mMinWordLength);
	}

	private void loadPreferences(Context c) {
		SharedPreferences prefs = PreferenceManager
				.getDefaultSharedPreferences(c);
		mLanguage = prefs.getString("dict", "MK");

		String str = prefs.getString("boardSize", "16");
		mBoardSize = Integer.parseInt(str);
		setMinWordLength(mBoardSize);
		str = prefs.getString("maxTimeRemaining", "180");
		mMaxTimeRemaining = 100 * Integer.parseInt(str);

		if (prefs.getBoolean("soundsEnabled", false)) {
			initSoundPool(c);
		}
	}

	public void initializeDictionary() {
		// Log.d(TAG,"initializeDictionary: " + board.getSize());
		long maskMk = 0;
		long neighborsMaskMk[] = new long[31];
		int maskEn = 0;
		int neighborsMaskEn[] = new int[26];
		CompressedTrieMk dictMk;
		CompressedTrieEn dictEn;

		for (int i = 0; i < mBoard.getSize(); i++) {
			if (mLanguage.equals("MK")) {
				int ival = LettersMap.cutoi(mBoard.elementAt(i).charAt(0));
				maskMk |= 1 << ival;

				for (int j = 0; j < mBoard.getSize(); j++) {
					if ((mBoard.transitions(i) & (1L << j)) != 0) {
						neighborsMaskMk[ival] |= 1L << LettersMap.cutoi(mBoard
								.elementAt(j).charAt(0));
					}
				}
			} else {
				int ival = LettersMap.ctoi(mBoard.elementAt(i).charAt(0));
				maskEn |= 1 << ival;

				for (int j = 0; j < mBoard.getSize(); j++) {
					if ((mBoard.transitions(i) & (1 << j)) != 0) {
						neighborsMaskEn[ival] |= 1 << LettersMap.ctoi(mBoard
								.elementAt(j).charAt(0));
					}
				}
			}
		}
		Log.d(TAG, "Transitions finished");
		/*
		 * for (int i = 0; i < 31; i++) { Log.d(TAG, "neighborMask: " +
		 * Long.toHexString(neighborMasks[i])); }
		 */
		try {
			InputStream is = null;
			if (mLanguage.equals("MK")) {
				is = mContext.getResources().openRawResource(R.raw.mk);
				dictMk = new CompressedTrieMk(is, maskMk, neighborsMaskMk);
				mSolutions = dictMk.solver(mBoard, new WordFilter() {
					public boolean isWord(String w) {
						return w.length() >= mMinWordLength;
					}
				});
				Log.d(TAG, "Dictionary loaded: " + dictMk.getNodesCount());
			} else {
				is = mContext.getResources().openRawResource(R.raw.en);
				dictEn = new CompressedTrieEn(is, maskEn, neighborsMaskEn);
				mSolutions = dictEn.solver(mBoard, new WordFilter() {
					public boolean isWord(String w) {
						return w.length() >= mMinWordLength;
					}
				});
				Log.d(TAG, "Dictionary loaded: " + dictEn.getNodesCount());
			}

		} catch (IOException e) {
			Log.e(TAG, "initializeDictionary", e);
		} catch (Exception e) {
			Log.d(TAG, e.getMessage(), e);
		}
	}

	public Board getBoard() {
		return mBoard;
	}

	public int getMaxTimeRemaining() {
		return mMaxTimeRemaining;
	}

	public int getWordCount() {
		return mWordCount;
	}

	public int getMinWordLength() {
		return mMinWordLength;
	}

	public int getMaxWordCount() {
		return mSolutions.size();
	}

	public ListIterator<String> listIterator() {
		return mWordsList.listIterator();
	}

	public ListIterator<String> guessedWords() {
		return mGuessedWordsList.listIterator();
	}

	public List<String> getWordsList() {
		return mWordsList;
	}

	public Iterator<String> uniqueListIterator() {
		return mWordsUsed.iterator();
	}

	public GameStatus getStatus() {
		return mStatus;
	}

	public LinkedHashMap<String, Solution> getSolutions() {
		return mSolutions;
	}

	public boolean start() {
		if (mStatus != GameStatus.GAME_STARTING) {
			return true;
		}

		mStart = new Date();
		mStatus = GameStatus.GAME_RUNNING;

		return true;
	}

	public void pause() {
		if (mStatus == GameStatus.GAME_RUNNING)
			mStatus = GameStatus.GAME_PAUSED;
	}

	public void unpause() {
		unpause(false);
	}

	public void unpause(boolean adjustTime) {
		if (adjustTime) {
			mStatus = GameStatus.GAME_RUNNING;
		} else {
			mStatus = GameStatus.GAME_RUNNING;
			mMaxTime = mTimeRemaining;
			mStart = new Date();
		}
	}

	public void endNow() {
		// Log.d(TAG,"endNow");
		mTimeRemaining = 0;
	}

	public void nextLevel() {
		mStatus = GameStatus.GAME_STARTING;
		mWordCount = 0;
		mCurrentLevel++;
		loadPreferences(mContext);
		generateBoard();

		mTimeRemaining = getMaxTimeRemaining();
		mMaxTime = getMaxTimeRemaining();
		Log.d(TAG, "maxTimeRemaining: " + mTimeRemaining);
		mWordsList = new LinkedList<String>();
		mWordsUsed = new LinkedHashSet<String>();
		mGuessedWordsList = new LinkedList<String>();
		mLevelPoints = 0;
		setLevelPoints();
	}

	public void save(SharedPreferences.Editor editor) {
		editor.putInt("boardSize", mBoard.getSize());
		editor.putString("gameBoard", mBoard.toString());
		editor.putInt("timeRemaining", mTimeRemaining);
		editor.putInt("maxTimeRemaining", getMaxTimeRemaining());
		editor.putString("words", wordListToString());
		editor.putString("dict", mLanguage);
		editor.putString("guessed_words", guessedWordsListToString());
		editor.putInt("score", getPoints());
		editor.putInt("level", mCurrentLevel);
		editor.putInt("level_total", mLevelTotalPoints);
		editor.putInt("wordCount", mWordCount);
		editor.putBoolean("activeGame", true);
		editor.commit();
	}

	public void save(Bundle bun) {
		bun.putInt("boardSize", mBoard.getSize());
		bun.putString("gameBoard", mBoard.toString());
		bun.putInt("timeRemaining", mTimeRemaining);
		bun.putInt("maxTimeRemaining", getMaxTimeRemaining());
		bun.putString("words", wordListToString());
		bun.putString("dict", mLanguage);
		bun.putString("guessed_words", guessedWordsListToString());
		bun.putInt("wordCount", mWordCount);
		bun.putInt("score", getPoints());
		bun.putInt("level", mCurrentLevel);
		bun.putInt("level_total", mLevelTotalPoints);
		bun.putLong("startTime", mStart.getTime());
		bun.putString("status", mStatus.toString());
		bun.putBoolean("activeGame", true);
	}

	private String wordListToString() {
		StringBuilder sb = new StringBuilder();
		ListIterator<String> li = mWordsList.listIterator();

		while (li.hasNext()) {
			String w = li.next();
			sb.append(w);
			if (li.hasNext()) {
				sb.append(",");
			}
		}

		return sb.toString();
	}

	private String guessedWordsListToString() {
		StringBuilder sb = new StringBuilder();
		ListIterator<String> li = mGuessedWordsList.listIterator();
		while (li.hasNext()) {
			sb.append(li.next());
			if (li.hasNext()) {
				sb.append(",");
			}
		}
		return sb.toString();
	}

	public void addWord(String word) {
		if (mStatus != GameStatus.GAME_RUNNING) {
			return;
		}
		String cap = word.toUpperCase();
		mWordsList.addFirst(cap);

		if (isWord(cap)) {
			if (mWordsUsed.contains(cap)) {
				// Word has been found before
				playSound(mSoundDuplicate);
			} else {
				// Word has not been found before
				mWordCount++;
				int wp = wordPoints(cap.length());
				mPoints += wp;
				mLevelPoints += wp;
				mGuessedWordsList.add(cap);
				playSound(mSoundHit);
			}
		} else {
			// Word is not really a word
			playSound(mSoundMiss);
		}
		mWordsUsed.add(cap);
	}

	public void playTouchSound() {
		playSound(mSoundTap);
	}

	private void initSoundPool(Context c) {
		mSoundPool = new SoundPool(3, AudioManager.STREAM_MUSIC, 100);
		mSoundTap = mSoundPool.load(c, R.raw.tap_sound, 1);
		mSoundMiss = mSoundPool.load(c, R.raw.miss_sound, 1);
		mSoundHit = mSoundPool.load(c, R.raw.hit, 1);
		mSoundDuplicate = mSoundPool.load(c, R.raw.duplicate, 1);
		mSoundClick = mSoundPool.load(c, R.raw.click, 1);
		mSoundClack = mSoundPool.load(c, R.raw.clack, 1);
		mSoundEndLevel = mSoundPool.load(c, R.raw.end_level, 1);
		mAudioManager = (AudioManager) c
				.getSystemService(Context.AUDIO_SERVICE);
	}

	private void playSound(int soundId) {
		if (mSoundPool != null) {
			int streamVolume = mAudioManager
					.getStreamVolume(AudioManager.STREAM_MUSIC);
			mSoundPool.play(soundId, streamVolume, streamVolume, 1, 0, 1f);
		}
	}

	public void rotateBoard() {
		mBoard.rotate();
		if (mRotateHandler != null)
			mRotateHandler.onRotate();
	}

	public boolean isWord(String word) {
		return mSolutions.containsKey(word);
	}

	@Override
	public int tick() {
		mTimeRemaining--;
		if (mTimeRemaining <= 0) {
			playSound(mSoundEndLevel);
			mStatus = GameStatus.GAME_FINISHED;
			mTimeRemaining = 0;
		} else {
			Date now = new Date();
			mTimeRemaining = Math.max(0, mMaxTime
					- (int) (now.getTime() - mStart.getTime()) / 10);
			if (mTimeRemaining < 1000) {
				if (mTimeRemaining / 100 < mSeconds || mSeconds == 0) {
					mSeconds = mTimeRemaining / 100;
					if (mSeconds % 2 == 0) {
						playSound(mSoundClick);
					} else {
						playSound(mSoundClack);
					}
				}
			}
			if (mPoints >= mLevelTotalPoints) {
				mStatus = GameStatus.GAME_NEW_LEVEL;
				mPoints += mTimeRemaining / 100;
				mTimeRemained = mTimeRemaining / 100;
				mTimeRemaining = 0;
			}
		}
		return mTimeRemaining;
	}

	public interface RotateHandler {
		public void onRotate();
	}

}
